﻿// HSS.ServiceModel.Internal.VirtualServiceValidator.cs
// ----------------------------------------------------------------------------
// Licensed under the MIT license
// http://www.opensource.org/licenses/mit-license.html
// ----------------------------------------------------------------------------
// HighSpeed-Solutions, LLC
// Copyright (c) 2001-2010
// ----------------------------------------------------------------------------
// File:       VirtualServiceValidator.cs
// Author:     HSS\gbanta
// Created:    08/12/2010
// Modified:   05/02/2011
// ----------------------------------------------------------------------------
namespace HSS.ServiceModel.Internal
{
	#region Using Directives
	using System;
	using System.Collections.Generic;
	using System.Reflection;
	using System.ServiceModel;
	using System.ServiceModel.Channels;
	using System.ServiceModel.Dispatcher;
	using System.Web;
	using System.Linq;
	using System.Security.Principal;
	using System.Threading;
	using System.Web.Security;
	#endregion

	#region ServiceValidator
	/// <summary>
	/// VirtualServiceValidator ensures the current Service Method being
	/// requested is secure (SSL), has a valid application token
	/// and is authenticted prior to the Service receiving it's message.
	/// </summary>
#if !DEBUG
	[System.Diagnostics.DebuggerStepThroughAttribute()]
#endif
	internal sealed class VirtualServiceValidator : IDispatchMessageInspector
	{
		#region Constants
		private const string CredentialsHeader = "VirtualServiceCredentials";
		private const string TokenHeader = "VirtualServiceToken";
		private const string HeaderNamespace = "http://highspeed-solutions.net/ServiceModel/v100";
		#endregion

		#region Fields
		private readonly Type inspectingType;
		private readonly string serviceTypeName;
		private readonly bool bypassAuthenticationForType;
		private readonly AuthorizationAttribute typeAuthorization;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="serviceType">The Type of the Service being inspected.</param>
		public VirtualServiceValidator(Type serviceType)
		{
			Guard.ArgumentNotNull(serviceType, "serviceType");
			this.inspectingType = serviceType;
			this.serviceTypeName = serviceType.FullName;
			this.bypassAuthenticationForType = this.BypassAuthenticationForType();
			this.typeAuthorization = this.GetAuthorizationForType();
		}
		#endregion

		#region IDispatchMessageInspector
		/// <summary>
		/// When overridden in a derived class, validates the request message.
		/// </summary>
		/// <param name="request">The message to evaluate</param>
		/// <param name="channel">The channel</param>
		/// <param name="instanceContext">The instance context</param>
		/// <returns>null</returns>
		public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
		{
			this.ValidateRequest(request);
			return null;
		}
		/// <summary>
		/// When overridden in a derived class, allows for additional 
		/// processing of the response message.
		/// </summary>
		/// <param name="reply">The response message</param>
		/// <param name="correlationState">Any value created in <c>AfterReceiveRequest</c></param>
		public void BeforeSendReply(ref Message reply, object correlationState)
		{
		}
		#endregion

		#region Methods

		void ValidateRequest(Message request)
		{
			#region Parse Request

			string errorMessage;

			var context = HttpContext.Current;
			if (null == context)
				throw new InvalidChannelBindingException("Missing HttpContext, and is required.");

			var uri = context.Request.Url;
			var url = uri.ToString();
			var referrer = "unknown";
			if (null != context.Request && null != context.Request.UrlReferrer)
				referrer = context.Request.UrlReferrer.ToString();

			var action = GetRequestAction(request);
			var authForAction = GetAuthorizationForAction(action);

			#endregion

			if (!IsValidApplication(request))
			{
				errorMessage = string.Format(Strings.ServiceFailure_UnknownApplication, action, serviceTypeName, url, referrer);
				throw new FaultException(errorMessage);
			}

			var scheme = uri.Scheme;
			if (VirtualServiceProvider.RequiresSSL && scheme != Uri.UriSchemeHttps)
				throw new UnauthorizedAccessException();

			if (!this.bypassAuthenticationForType && !this.BypassAuthenticationForAction(action))
			{
				#region Custom Authorization
				string[] usersRoles;
				if (IsUserAuthenticatedCustom(request, out usersRoles))
				{
					//
					// Validate Type Authorization Roles
					//					
					if (null != this.typeAuthorization && !this.typeAuthorization.IsUserInRole(usersRoles))
					{
						errorMessage = string.Format(Strings.ServiceFailure_UnauthorizedRequest, action, serviceTypeName, url, referrer);
						throw new FaultException(errorMessage);
					}

					//
					// Validate Method Authorization Roles
					//					
					if (null == authForAction)
						return; // No AuthorizationAttribute defined.
					if (!authForAction.IsUserInRole(usersRoles))
					{
						errorMessage = string.Format(Strings.ServiceFailure_UnauthorizedRequest, action, serviceTypeName, url, referrer);
						throw new FaultException(errorMessage);
					}

					return;
				}
				#endregion

				#region Web Authorization
				if (IsUserAuthenticated(context))
				{
					//
					// Validate Type Authorization Roles
					//
					if (null != this.typeAuthorization && !this.typeAuthorization.IsUserInRole(context.User))
					{
						errorMessage = string.Format(Strings.ServiceFailure_UnauthorizedRequest, action, serviceTypeName, url, referrer);
						throw new FaultException(errorMessage);
					}

					//
					// Validate Method Authorization Roles
					//
					if (null == authForAction)
						return; // No AuthorizationAttribute defined.
					if (!authForAction.IsUserInRole(context.User))
					{
						errorMessage = string.Format(Strings.ServiceFailure_UnauthorizedRequest, action, serviceTypeName, url, referrer);
						throw new FaultException(errorMessage);
					}

					return;
				}
				#endregion

				errorMessage = string.Format(Strings.ServiceFailure_UnauthenticatedRequest, action, serviceTypeName, url, referrer);
				throw new FaultException(errorMessage);
			}
		}

		bool BypassAuthenticationForType()
		{
			object[] bypassAuthentication = this.inspectingType.GetCustomAttributes(typeof(BypassAuthenticationAttribute), false);
			return (null != bypassAuthentication && bypassAuthentication.Length > 0);
		}
		bool BypassAuthenticationForAction(string action)
		{
			var method = inspectingType.GetMethod(action);
			if (null == method)
				return false;
			var bypassAuthentication = method.GetCustomAttributes(typeof(BypassAuthenticationAttribute), false);
			return null != bypassAuthentication && bypassAuthentication.Length > 0;
		}
		AuthorizationAttribute GetAuthorizationForType()
		{
			var authorizationAttributes = inspectingType.GetCustomAttributes(typeof(AuthorizationAttribute), false);
			if (null != authorizationAttributes && authorizationAttributes.Length > 0)
				return (AuthorizationAttribute)authorizationAttributes[0];
			return null;
		}
		AuthorizationAttribute GetAuthorizationForAction(string action)
		{
			var method = this.inspectingType.GetMethod(action);
			if (null == method)
				return null;
			var authorizationAttributes = method.GetCustomAttributes(typeof(AuthorizationAttribute), false);
			if (null != authorizationAttributes && authorizationAttributes.Length > 0)
				return (AuthorizationAttribute)authorizationAttributes[0];
			return null;
		}

		private static bool IsValidApplication(Message request)
		{
			if (null != request)
			{
				var requestApplicationToken = string.Empty;
				try { requestApplicationToken = request.Headers.GetHeader<string>(TokenHeader, HeaderNamespace); }
				catch { requestApplicationToken = string.Empty; }
				if (string.IsNullOrEmpty(requestApplicationToken))
					return false;

				var validApplicationToken = VirtualServiceProvider.ServiceToken;
				if (string.IsNullOrEmpty(validApplicationToken))
					return false;

				return validApplicationToken.Equals(requestApplicationToken, StringComparison.Ordinal);
			}
			return false;
		}

		private static bool IsUserAuthenticated(HttpContext context)
		{
			var isAuthenticated = false;
			var currentUser = GetCurrentUser(context);
			if (currentUser != null)
			{
				var identity = currentUser.Identity;
				if (identity != null)
					isAuthenticated = identity.IsAuthenticated;
			}
			return isAuthenticated;
		}
		private static bool IsUserAuthenticatedCustom(Message request, out string[] roles)
		{
			roles = new string[] { };
			if (null != request)
			{
				var sep = "\u2521";
				var token = string.Empty;
				try
				{
					var index = request.Headers.FindHeader(CredentialsHeader, HeaderNamespace);
					if (index >= 0)
						token = request.Headers.GetHeader<string>(index);
				}
				catch { token = string.Empty; }
				if (string.IsNullOrEmpty(token))
					return false;

				var b = System.Convert.FromBase64String(token);
				var s = System.Text.UTF8Encoding.UTF8.GetString(b);
				var parts = s.Split(sep.ToArray(), StringSplitOptions.None);
				if (null != parts && parts.Length == 2)
				{
					var userName = parts[0];
					var password = parts[1];
					if (Roles.Enabled)
						roles = Roles.GetRolesForUser(userName);

					return Membership.ValidateUser(userName, password);
				}
			}
			return false;
		}

		private static string GetRequestAction(Message request)
		{
			var actionUri = new Uri(request.Headers.Action, UriKind.Absolute);
			return actionUri.Segments[actionUri.Segments.Length - 1];
		}
		private static IPrincipal GetCurrentUser(HttpContext context)
		{
			if (context != null)
				return context.User;
			return Thread.CurrentPrincipal;
		}

		#endregion
	}
	#endregion
}